﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Graphics.Implementation;
using Tesla.Math;
using Tesla.Util;
using D3D = SlimDX.Direct3D10;
using SDX = SlimDX;

namespace Tesla.Direct3D10.Graphics.Implementation {
    /// <summary>
    /// Concrete Direct3D10 implementation of <see cref="Texture3D"/>.
    /// </summary>
    public sealed class D3D10Texture3DImplementation : Texture3DImplementation {
        private D3D10Renderer _renderer;
        private D3D.Device _graphicsDevice;
        private D3D.ShaderResourceView _shaderResourceView;
        private D3D.Texture3D _texture3D;
        private D3D.Texture3D _staging;
        private D3D.ResourceUsage _usage;
        private int _mipCount;

        /// <summary>
        /// Gets the D3D10Renderer that created this implementation.
        /// </summary>
        internal D3D10Renderer Renderer {
            get {
                return _renderer;
            }
        }
        
        /// <summary>
        /// Gets the D3D10 ShaderResourceView.
        /// </summary>
        internal D3D.ShaderResourceView D3D10ShaderResourceView {
            get {
                return _shaderResourceView;
            }
        }

        /// <summary>
        /// Gets the D3D10 Texture3D.
        /// </summary>
        internal D3D.Texture3D D3D10Texture3D {
            get {
                return _texture3D;
            }
        }

        /// <summary>
        /// Gets the format of the texture resource.
        /// </summary>
        public override int MipCount {
            get {
                return _mipCount;
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10Texture3DImplementation"/>.
        /// </summary>
        /// <param name="renderer">The D3D0 renderer.</param>
        /// <param name="width">The width of the texture.</param>
        /// <param name="height">The height of the texture.</param>
        /// <param name="depth">The depth of the texture.</param>
        /// <param name="genMipMaps">True if mip map subresources should be generated or not.</param>
        /// <param name="format">The surface format.</param>
        /// <param name="data">The initial data for the first mip map.</param>
        internal D3D10Texture3DImplementation(D3D10Renderer renderer, int width, int height, int depth, bool genMipMaps, SurfaceFormat format, DataBuffer data)
            : base(width, height, depth, format) {
            //Set common properties
            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;

            //Do we want to generate mip maps, and do we have the data to do so? (if not, cancel mip map generation)
            bool canGenMipmaps = (genMipMaps && (data != null));

            D3D.Texture3DDescription descTex = new D3D.Texture3DDescription();
            descTex.Width = width;
            descTex.Height = height;
            descTex.Depth = depth;
            descTex.Usage = _usage = D3D.ResourceUsage.Default;
            descTex.CpuAccessFlags = D3D.CpuAccessFlags.None;
            descTex.Format = D3D10Helper.ToD3DSurfaceFormat(format);
            descTex.MipLevels = (genMipMaps) ? 0 : 1;

            //Set mip map generation params
            if(canGenMipmaps) {
                descTex.BindFlags = D3D.BindFlags.ShaderResource | D3D.BindFlags.RenderTarget;
                descTex.OptionFlags = D3D.ResourceOptionFlags.GenerateMipMaps;
            } else {
                descTex.BindFlags = D3D.BindFlags.ShaderResource;
                descTex.OptionFlags = D3D.ResourceOptionFlags.None;
            }

            //Create the texture and shader view
            _texture3D = new D3D.Texture3D(_graphicsDevice, descTex);
            _shaderResourceView = new D3D.ShaderResourceView(_graphicsDevice, _texture3D);

            //Add to tracker
            _renderer.Resources.AddTrackedObject(_texture3D.ComPointer, this);
            _renderer.Resources.AddTrackedObject(_shaderResourceView.ComPointer, this);

            //Set the final mip count
            _mipCount = _texture3D.Description.MipLevels;

            //Now set the initial data if its present
            if(data != null) {
                try {
                    this.SetData<byte>(data.ByteDataCopy, 0, 0, width, 0, height, 0, depth, 0, data.ElementSizeInBytes * data.Length);

                    if(genMipMaps) {
                        _graphicsDevice.GenerateMips(_shaderResourceView);
                    }

                    //Dispose of the staging texture
                    if(_staging != null) {
                        _staging.Dispose();
                        _staging = null;
                    }
                } catch(Exception e) {
                    Dispose();
                    throw new TeslaException("Error setting Texture data.", e);
                }
            }
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="D3D10Texture3DImplementation"/> is reclaimed by garbage collection.
        /// </summary>
        ~D3D10Texture3DImplementation() {
            Dispose(false);
        }

        /// <summary>
        /// Gets the data from the texture.
        /// </summary>
        /// <typeparam name="T">Type of data in the array</typeparam>
        /// <param name="data">The array of data</param>
        /// <param name="mipLevel">Mip map level to read from</param>
        /// <param name="left">Right-most width position in the texture at which to acess. (0 or greater)</param>
        /// <param name="right">Right-most width position in the texture at which to acess. (width or less)</param>
        /// <param name="top">Top-most height position in the texture at which to acess. (0 or greater)</param>
        /// <param name="bottom">Bottom-most height position in the texture at which to acess. (height or less)</param>
        /// <param name="front">Front-most depth position in the texture at which to acess. (0 or greater)</param>
        /// <param name="back">Back-most depth position in the texture at which to acess. (depth or less)</param>
        /// <param name="startIndex">Starting index in the array to start reading from.</param>
        /// <param name="elementCount">Number of elements to write.</param>
        /// <exception cref="System.ArgumentException">Thrown if the format byte size of the type to read and the texture do not match, the subimage
        /// dimensions are invalid, or if the total size to read and the total size in the texture do not match</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if there was an error reading from the texture.</exception>
        public override void GetData<T>(T[] data, int mipLevel, int left, int right, int top, int bottom, int front, int back, int startIndex, int elementCount) {
            if(_texture3D == null || _texture3D.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            if(mipLevel < 0 || mipLevel >= _mipCount) {
                throw new ArgumentOutOfRangeException("mipLevel", String.Format("Mip level is out of range. Must be between 0 and {0}.", _mipCount.ToString()));
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);

            int formatSize = D3D10Helper.FormatSize(base.Format);
            int elemSize = MemoryHelper.SizeOf<T>();
            CheckSizes(formatSize, elemSize);

            //Calc subresource dimensions 
            int width = (int) MathHelper.Max(1, base.Width >> mipLevel);
            int height = (int) MathHelper.Max(1, base.Height >> mipLevel);
            int depth = (int) MathHelper.Max(1, base.Depth >> mipLevel);

            //Ensure box dimensions
            CheckBox(left, right, top, bottom, front, back, ref width, ref height, ref depth);

            CheckTotalSize(base.Format, ref formatSize, ref width, ref height, ref depth, elemSize, elementCount);

            //Create staging
            CreateStaging();

            try {
                int row = left;
                int col = top;
                int slice = front;

                //Do resource copy
                if(base.Format == SurfaceFormat.DXT1 || base.Format == SurfaceFormat.DXT3 || base.Format == SurfaceFormat.DXT5) {
                    _graphicsDevice.CopyResource(_texture3D, _staging);
                } else {
                    D3D.ResourceRegion region = new D3D.ResourceRegion();
                    region.Left = col;
                    region.Right = right;
                    region.Top = row;
                    region.Bottom = bottom;
                    region.Front = slice;
                    region.Back = back;
                    _graphicsDevice.CopySubresourceRegion(_texture3D, mipLevel, region, _staging, mipLevel, col, row, slice);
                }

                SDX.DataBox dataBox = _staging.Map(mipLevel, D3D.MapMode.Read, D3D.MapFlags.None);
                SDX.DataStream ds = dataBox.Data;
                int rowPitch = dataBox.RowPitch;
                int slicePitch = dataBox.SlicePitch;
                int index = startIndex;
                int count = elementCount;

                //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));
                //Go slice by slice
                for(int k = slice; k < back; k++) {
                    //Go row by row
                    for(int i = row; i < bottom; i++) {
                        //Set the position
                        ds.Position = (k * slicePitch) + (i * rowPitch) + (col * formatSize);
                        //Break if we've run out of elements or on the very last row that isn't complete
                        if(count <= 0) {
                            break;
                        } else if(count < actWidth) {
                            ds.ReadRange<T>(data, index, count);
                            break;
                        }
                        //Otherwise, copy the whole row and increment/decrement the index and count
                        ds.ReadRange<T>(data, index, actWidth);
                        index += actWidth;
                        count -= actWidth;
                    }
                }
                _staging.Unmap(mipLevel);
            } catch(Exception e) {
                throw new TeslaException("Error reading from D3D10 Texture3D.", e);
            }
        }

        /// <summary>
        /// Sets the data from the texture.
        /// </summary>
        /// <typeparam name="T">Type of data in the array</typeparam>
        /// <param name="data">The array of data</param>
        /// <param name="mipLevel">Mip map level to read from</param>
        /// <param name="left">Right-most width position in the texture at which to acess. (0 or greater)</param>
        /// <param name="right">Right-most width position in the texture at which to acess. (width or less)</param>
        /// <param name="top">Top-most height position in the texture at which to acess. (0 or greater)</param>
        /// <param name="bottom">Bottom-most height position in the texture at which to acess. (height or less)</param>
        /// <param name="front">Front-most depth position in the texture at which to acess. (0 or greater)</param>
        /// <param name="back">Back-most depth position in the texture at which to acess. (depth or less)</param>
        /// <param name="startIndex">Starting index in the array to start reading from.</param>
        /// <param name="elementCount">Number of elements to write.</param>
        /// <exception cref="System.ArgumentException">Thrown if the format byte size of the type to write and the texture do not match, the subimage
        /// dimensions are invalid, or if the total size to write and the total size in the texture do not match</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if there was an error writing to the texture.</exception>
        public override void SetData<T>(T[] data, int mipLevel, int left, int right, int top, int bottom, int front, int back, int startIndex, int elementCount) {
            if(_texture3D == null ||_texture3D.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            if(mipLevel < 0 || mipLevel >= _mipCount) {
                throw new ArgumentOutOfRangeException("mipLevel", String.Format("Mip level is out of range. Must be between 0 and {0}.", _mipCount.ToString()));
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);

            int formatSize = D3D10Helper.FormatSize(base.Format);
            int elemSize = MemoryHelper.SizeOf<T>();
            CheckSizes(formatSize, elemSize);

            //Calc subresource dimensions 
            int width = (int) MathHelper.Max(1, base.Width >> mipLevel);
            int height = (int) MathHelper.Max(1, base.Height >> mipLevel);
            int depth = (int) MathHelper.Max(1, base.Depth >> mipLevel);

            //Ensure box dimensions
            CheckBox(left, right, top, bottom, front, back, ref width, ref height, ref depth);

            CheckTotalSize(base.Format, ref formatSize, ref width, ref height, ref depth, elemSize, elementCount);

            //Create staging for non-dynamic textures
            if(_usage == D3D.ResourceUsage.Default) {
                CreateStaging();
            }

            try {
                if(_usage == D3D.ResourceUsage.Default) {
                    SDX.DataBox dataBox = _staging.Map(mipLevel, D3D.MapMode.Write, D3D.MapFlags.None);
                    SDX.DataStream ds = dataBox.Data;
                    int row = left;
                    int col = top;
                    int slice = front;
                    int rowPitch = dataBox.RowPitch;
                    int slicePitch = dataBox.SlicePitch;

                    int index = startIndex;
                    int count = elementCount;
                    //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                    int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));
                    //Go slice by slice
                    for(int k = slice; k < back; k++) {
                        //Go row by row
                        for(int i = row; i < bottom; i++) {
                            //Set the position
                            ds.Position = (k * slicePitch) + (i * rowPitch) + (col * formatSize);
                            //Break if we've run out of elements or on the very last row that isn't complete
                            if(count <= 0) {
                                break;
                            } else if(count < actWidth) {
                                ds.WriteRange<T>(data, index, count);
                                break;
                            }
                            //Otherwise, copy the whole row and increment/decrement the index and count
                            ds.WriteRange<T>(data, index, actWidth);
                            index += actWidth;
                            count -= actWidth;
                        }
                    }
                    _staging.Unmap(mipLevel);

                    //Do resource copy
                    if(base.Format == SurfaceFormat.DXT1 || base.Format == SurfaceFormat.DXT3 || base.Format == SurfaceFormat.DXT5) {
                        _graphicsDevice.CopyResource(_staging, _texture3D);
                    } else {
                        D3D.ResourceRegion region = new D3D.ResourceRegion();
                        region.Left = col;
                        region.Right = right;
                        region.Top = row;
                        region.Bottom = bottom;
                        region.Front = slice;
                        region.Back = back;
                        _graphicsDevice.CopySubresourceRegion(_staging, mipLevel, region, _texture3D, mipLevel, col, row, slice);
                    }
                } else {
                    SDX.DataBox dataBox = _texture3D.Map(mipLevel, D3D.MapMode.Write, D3D.MapFlags.None);
                    SDX.DataStream ds = dataBox.Data;
                    int row = left;
                    int col = top;
                    int slice = front;
                    int rowPitch = dataBox.RowPitch;
                    int slicePitch = dataBox.SlicePitch;

                    int index = startIndex;
                    int count = elementCount;
                    //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                    int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));
                    //Go slice by slice
                    for(int k = slice; k < back; k++) {
                        //Go row by row
                        for(int i = row; i < bottom; i++) {
                            //Set the position
                            ds.Position = (k * slicePitch) + (i * rowPitch) + (col * formatSize);
                            //Break if we've run out of elements or on the very last row that isn't complete
                            if(count <= 0) {
                                break;
                            } else if(count < actWidth) {
                                ds.WriteRange<T>(data, index, count);
                                break;
                            }
                            //Otherwise, copy the whole row and increment/decrement the index and count
                            ds.WriteRange<T>(data, index, actWidth);
                            index += actWidth;
                            count -= actWidth;
                        }
                    }
                    _texture3D.Unmap(mipLevel);
                }
            } catch(Exception e) {
                throw new TeslaException("Error writing to D3D10 Texture3D.", e);
            }
        }

        private static void CheckSizes(int formatSize, int elementSize) {
            if(formatSize != elementSize) {
                if((formatSize <= elementSize) || ((formatSize % elementSize) != 0)) {
                    throw new ArgumentException("Element format size does not match texture's expected format size.");
                }
            }
        }

        private static void CheckBox(int left, int right, int top, int bottom, int front, int back, ref int width, ref int height, ref int depth) {
            if(left < 0 || top < 0 || front < 0) {
                throw new ArgumentException("Invalid Box: Left, top, and front cannot be less than zero.");
            }

            if(right > width || bottom > height || back > depth) {
                throw new ArgumentException("Invalid Box: Right, bottom, or back cannot be greater than width, height, and depth");
            }

            if(left > right || top > bottom || front > back) {
                throw new ArgumentException("Invalid Box: Left, top, and front cannot be greater than right, bottom, and back respectively.");
            }

            width = right - left;
            height = bottom - top;
            depth = back - front;
        }

        private static void CheckTotalSize(SurfaceFormat format, ref int formatSize, ref int width, ref int height, ref int depth, int elementSize, int elementCount) {
            if(format == SurfaceFormat.DXT1 || format == SurfaceFormat.DXT3 || format == SurfaceFormat.DXT5) {
                width = (width + 3) >> 2;
                height = (height + 3) >> 2;
                depth = (depth + 3) >> 2;
                formatSize = (format == SurfaceFormat.DXT1) ? 8 : 16;
            }

            int totalSize = width * height * depth  * formatSize;
            if(elementSize * elementCount != totalSize) {
                throw new ArgumentException("Total size of data to write does not match expected total size.");
            }
        }

        private void CreateStaging() {
            if(_staging == null) {

                D3D.Texture3DDescription oDesc = _texture3D.Description;

                D3D.Texture3DDescription desc = new D3D.Texture3DDescription();
                desc.BindFlags = D3D.BindFlags.None;
                desc.CpuAccessFlags = D3D.CpuAccessFlags.Write | D3D.CpuAccessFlags.Read;
                desc.Format = D3D10Helper.ToD3DSurfaceFormat(base.Format);
                desc.Height = base.Height;
                desc.Width = base.Width;
                desc.Depth = base.Depth;
                desc.MipLevels = _mipCount;
                desc.Usage = D3D.ResourceUsage.Staging;

                _staging = new D3D.Texture3D(_graphicsDevice, desc);

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_staging.ComPointer, this);
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing) {
            if(!IsDisposed) {
                //Dispose of managed resources
                if(disposing) {
                    if(_shaderResourceView != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_shaderResourceView.ComPointer);
                        }
                        _shaderResourceView.Dispose();
                        _shaderResourceView = null;
                    }

                    if(_texture3D != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_texture3D.ComPointer);
                        }
                        _texture3D.Dispose();
                        _texture3D = null;
                    }

                    if(_staging != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_staging.ComPointer);
                        }
                        _staging.Dispose();
                        _staging = null;
                    }
                }
                base.Dispose(disposing);
            }
        }
    }
}
